package pfc.modeloer;

import java.io.Serializable;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;

import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.ddlutils.dynabean.DynaClassCache;
import org.apache.ddlutils.dynabean.SqlDynaClass;
import org.apache.ddlutils.model.Column;
import org.apache.ddlutils.model.Database;
import org.apache.ddlutils.model.ForeignKey;
import org.apache.ddlutils.model.ModelException;
import org.apache.ddlutils.model.Table;

public class ERModel implements Serializable, Cloneable {

    /** Unique ID for serialization purposes. */
    private static final long serialVersionUID = -3160443396757573868L;
    /** The name of the database model. */
    private String _name;
    /** The method for generating primary keys (currently ignored). */
    private String _idMethod;
    /** The version of the model. */
    private String _version;
    /** The tables. */
    private ArrayList<EREntity> _entidades = new ArrayList<EREntity>();
    //private ArrayList<ERRelationShip> _relaciones = new ArrayList<ERRelationShip>();
    private LinkedHashSet<ERRelationShip> _relaciones = new LinkedHashSet<ERRelationShip>();

    /** The dyna class cache for this model. */
    private transient DynaClassCache _dynaClassCache = null;
    private Database _db;
    private ERReferences _referencias = new ERReferences();
    private String _databaseName;

    public ERModel()
    {}

    public ERModel(Database db) {
        _db = db;
        _referencias = new ERReferences();
        _databaseName = db.getName();
        createFromDatabase(db);
    }

    public ERReferences getReferencias() {
        return _referencias;
    }



    public String getDatabaseName() {
        return _databaseName;
    }

    public void setDatabaseName(String _databaseName) {
        this._databaseName = _databaseName;
    }

   

    public String getName() {
        return _name;
    }

    public void setName(String name) {
        _name = name;
    }

    public String getVersion() {
        return _version;
    }

    public void setVersion(String version) {
        _version = version;
    }

    public String getIdMethod() {
        return _idMethod;
    }

    public void setIdMethod(String idMethod) {
        _idMethod = idMethod;
    }

    public int getEntityCount() {
        return _entidades.size();
    }

    public EREntity[] getEntityList() {
        return (EREntity[]) _entidades.toArray(new EREntity[_entidades.size()]);
    }

    public EREntity getEntity(int idx) {
        return (EREntity) _entidades.get(idx);
    }

    public void addEntity(EREntity entity) {
        if (entity != null) {
            _entidades.add(entity);
        }
    }

    public void addEntity(int idx, EREntity entity) {
        if (entity != null) {
            _entidades.add(idx, entity);
        }
    }

    public void addEntityList(Collection entities) {
        for (Iterator it = entities.iterator(); it.hasNext();) {
            addEntity((EREntity) it.next());
        }
    }

    public void removeEntity(EREntity entity) {
        if (entity != null) {
            _entidades.remove(entity);
        }
    }

    public void removeEntity(int idx) {
        _entidades.remove(idx);
    }

    public void initialize() throws ModelException {
        HashSet namesOfProcessedEntities = new HashSet();
        HashSet namesOfProcessedAttributes = new HashSet();
        HashSet namesOfProcessedFks = new HashSet();
        HashSet namesOfProcessedIndices = new HashSet();
        int tableIdx = 0;

        if ((getName() == null) || (getName().length() == 0)) {
            throw new ModelException("The database model has no name");
        }

        for (Iterator tableIt = _entidades.iterator(); tableIt.hasNext(); tableIdx++) {
            EREntity curTable = (EREntity) tableIt.next();

            if ((curTable.getName() == null) || (curTable.getName().length() == 0)) {
                throw new ModelException("The table nr. " + tableIdx + " has no name");
            }
            if (namesOfProcessedEntities.contains(curTable.getName())) {
                throw new ModelException("There are multiple tables with the name " + curTable.getName());
            }
            namesOfProcessedEntities.add(curTable.getName());

            namesOfProcessedAttributes.clear();
            namesOfProcessedFks.clear();
            namesOfProcessedIndices.clear();

            for (int idx = 0; idx < curTable.myGetAttribCount(); idx++) {
                ERAttrib column = curTable.myGetAttrib(idx);

                if ((column.getName() == null) || (column.getName().length() == 0)) {
                    throw new ModelException("The column nr. " + idx + " in table " + curTable.getName() + " has no name");
                }
                if (namesOfProcessedAttributes.contains(column.getName())) {
                    throw new ModelException("There are multiple column with the name " + column.getName() + " in the table " + curTable.getName());
                }
                namesOfProcessedAttributes.add(column.getName());

                if ((column.getType() == null) || (column.getType().length() == 0)) {
                    throw new ModelException("The column nr. " + idx + " in table " + curTable.getName() + " has no type");
                }
                if ((column.getTypeCode() == Types.OTHER) && !"OTHER".equalsIgnoreCase(column.getType())) {
                    throw new ModelException("The column nr. " + idx + " in table " + curTable.getName() + " has an unknown type " + column.getType());
                }
                namesOfProcessedAttributes.add(column.getName());
            }


        }
    }

    public EREntity findEntity(String name) {
        return findEntity(name, false);
    }

    public EREntity findEntity(String name, boolean caseSensitive) {
        for (Iterator iter = _entidades.iterator(); iter.hasNext();) {
            EREntity table = (EREntity) iter.next();

            if (caseSensitive) {
                if (table.getName().equals(name)) {
                    return table;
                }
            } else {
                if (table.getName().equalsIgnoreCase(name)) {
                    return table;
                }
            }
        }
        return null;
    }

    private DynaClassCache getDynaClassCache() {
        if (_dynaClassCache == null) {
            _dynaClassCache = new DynaClassCache();
        }
        return _dynaClassCache;
    }

    public void resetDynaClassCache() {
        _dynaClassCache = null;
    }

    public SqlDynaClass getDynaClassFor(DynaBean bean) {
        return getDynaClassCache().getDynaClass(bean);
    }

    public Object clone() throws CloneNotSupportedException {
        ERModel result = (ERModel) super.clone();

        result._name = _name;
        result._idMethod = _idMethod;
        result._version = _version;
        result._entidades = (ArrayList) _entidades.clone();

        return result;
    }

    public boolean equals(Object obj) {
        if (obj instanceof ERModel) {
            ERModel other = (ERModel) obj;

            // Note that this compares case sensitive
            return new EqualsBuilder().append(_name, other._name).append(_entidades, other._entidades).isEquals();
        } else {
            return false;
        }
    }

    public int hashCode() {
        return new HashCodeBuilder(17, 37).append(_name).append(_entidades).toHashCode();
    }

    public String toString() {
        StringBuffer result = new StringBuffer();

        result.append("Modelo ER [name=");
        result.append(getName());
        result.append("; ");
        result.append(getEntityCount());
        result.append(" tables]");

        return result.toString();
    }

    public String toVerboseString() {
        StringBuffer result = new StringBuffer();

        result.append("Database [");
        result.append(getName());
        result.append("] tables:");
        for (int idx = 0; idx < getEntityCount(); idx++) {
            result.append(" ");
            result.append(getEntity(idx).toVerboseString());
        }

        return result.toString();
    }

    private void createFromDatabase(Database db) {
        setIdMethod(db.getIdMethod());
        setName(db.getName());
        setVersion(db.getVersion());

        /*
         * Obtiene las referencias entre tablas y las guarda en un objeto  ERREfereces. Se usa como apoyo para la identificacion del tipo de entidad
         */
        for (Table t : db.getTables()) {
            if (t.getName().startsWith("_"))
                continue;
            EREntity e = new EREntity(this,t);
            addEntity(e);            
        }

        for (Table t : db.getTables()) {
            _referencias.addTableReferences(this,t);
        }




        System.out.println(_referencias.toString());
        identifyMasterEntities(db);
        identifyRelationShipEntities(db);
        identifyNormalEntities(db);


        //printReferencias();
        createRelationShips(db);
    }

    /**
     * Este metodo crea las relaciones entre tablas, identifica las entidades por su tipo conceptual
     *
     * Entidad Maestra: MASTER_ENTITY;
     * Entidad Normal: NORMAL_ENTITY;
     * Entidad Debil: DEPENDANT_ENTITY=3;
    Relacion RELATIONSHIP=4;
     */
    private void createRelationShips(Database db) {

        for (Table t : db.getTables()) {
            EREntity e = this.findEntity(t.getName());
            //int tipoEntidad = getTipoEntidad(t, e);

            //e.setEREntityType(tipoEntidad);
        }
    }

    /**
     * Recorro todas las tablas.
     * Si una tabla no tiene FK y es refenciada por mas de una tabla la entidad a la que represente se marcara como entidad maestra
     * Si una tabla no tiene FK y NO es referenciada por ninguna tabla, la entidad a la que representa se marcara como entidad normal.
     * Si una tabla tiene FK y solo referencia entidades maestras la entidad a la que represente se marcara como entidad normal
     * Si una tabla tiene FK y referencia entidades no maestras se marcara como entidad debil
     * Si una tabla tiene su PK formada solo por FK, representa una relacion N:M
     */
    private void identifyMasterEntities(Database db) {
        /*
         * Identifica las entidades maestras.
         * Son entidades maestras las que no referencian a ninguna otra entidad y son referenciadas por mas de una (entidad)
         */
        for (Table t : db.getTables()) {
            EREntity e = this.findEntity(t.getName());
            int referenciadoPor = _referencias.getTargetTableReferences(t.getName()).size();
            int referenciaA = t.getForeignKeyCount();

            if ((referenciadoPor >= 1) && (referenciaA == 0)) {
                e.setErElementType(ERElement.EntityType.Master);
            }
        }
    }

    private void identifyRelationShipEntities(Database db) {
        /*
         * Identifica las tablas que representan una relacion, son relaciones M:N implementadas como tablas
         * Una vez identificadas, las convierte en relaciones
         * Representan relaciones M:N aquellas tablas que su clave primaria esta compuesta por referencias a otras tablas
         */
        for (Table t : db.getTables()) {
            EREntity e1 = this.findEntity(t.getName());
            boolean sw = false;
            boolean nextColumn = false;
            for (Column c : t.getPrimaryKeyColumns()) {
                boolean estaEnUnaFK = false;
                for (ForeignKey fk : t.getForeignKeys()) {
                    if (fk.hasLocalColumn(c)) {
                        estaEnUnaFK = true;
                        nextColumn = true;
                        sw = true;
                        break;
                    } else {
                        sw = false;
                    }
                }
                if (nextColumn == true) {
                    nextColumn = false;
                    continue;
                }
            }
            if (sw && e1.getErElementType()==ERElement.EntityType.Unknown)
            {
                e1.setErElementType(ERElement.EntityType.RelationShip);

                this.converToRelationShip(e1);
            }
        }
    }

    public void identifyNormalEntities(Database db) {
        /*
         * Identifica las Entidades Normales, que son las que referencian a otras entidades pero tienen clave primaria propia.
         */
        for (Table t : db.getTables()) {
            EREntity e = this.findEntity(t.getName());
            if (e==null)
            {
                /**
                 * Si no se encuentra la entidad es que se ha convertido en una relacion, salto a la siguiente tabla del bucle.
                 */
                continue;
            }
            boolean soloMaestras = true;
            for (ERReference ref : _referencias.getTargetTableReferences(t.getName())) {
                EREntity tmp = this.findEntity(t.getName());
                if (!(tmp.getErElementType()==ERElement.EntityType.Master)) {
                    soloMaestras = false;
                }
            }

            if (soloMaestras && e.getErElementType()==ERElement.EntityType.Unknown) {
                e.setErElementType(ERElement.EntityType.Normal);
                e.buildRelationShips();
                this.addRelationShips(e.getRelationShips());
            } else {
                if (e.getErElementType()==ERElement.EntityType.Unknown)
                {
                    e.setErElementType(ERElement.EntityType.Dependant);
                    e.buildRelationShips();
                    this.addRelationShips(e.getRelationShips());
                }
            }
        }

    }

    private void printReferencias() {
        for (EREntity e : this._entidades) {
            System.out.println(e.toVerboseString());
        }


        for (ERRelationShip rel : this._relaciones) {
            System.out.println(rel.toVerboseString());
        }
    }


    private void converToRelationShip(EREntity e1) {
        //this.removeEntity(e1);
        _relaciones.add(e1.convertIntoRelationShip());
    }

    private void addRelationShips(Collection<ERRelationShip> relationShips) {
        for (ERRelationShip rel: relationShips)
        {
            _relaciones.add(rel);
        }
    }


    public Collection<ERRelationShip> getRelationShipList()
    {
        return _relaciones;
    }

    void addRelationShip(ERRelationShip r) {
        _relaciones.add(r);
    }

    public void setDatabaseModel(Database dbmodel) {
       _db = dbmodel;
       for (EREntity e: this._entidades)
       {
           e.setTable(dbmodel.findTable(e.getName()));
       }

    }

    public Iterable<ERReference> getReferencedByAttrib(EREntity tab, ERAttrib a) {

        HashMap<String,ERReference> resul = new HashMap<String,ERReference>();
        //ArrayList<ERReference> resul = new ArrayList<ERReference>();
        for (ERRelationShip rel:this._relaciones)
        {
            for (EREntity ent: rel.getMembers())
            {
                if (StringUtils.equals(ent.getName(), tab.getName()))
                {
                    for (ERReference ref: rel.getReferences()) {
                        if (StringUtils.equals(ref.sourceAttrib.getName(),a.getName()))
                        {
                            String key = ref.targetEntity.getName() + "." + ref.targetAttrib.getName();
                            resul.put(key,ref);
                        }
                            
                    }

                }
            }
        }

        return resul.values();
    }


    public boolean attribReferencesOtherEntity(Table tab, ERAttrib a)
    {
        for (ERReference ref: this._referencias.getSourceTableReferences(tab.getName()))
        {
            if (StringUtils.equals(ref.getSourceAttribName(),a.getName()))
            {
                return true;
            }

        }

        return false;
    }

    public Iterable<ERReference> getReferrerToAttrib(EREntity e, ERAttrib a) {
        HashMap<String,ERReference> resul = new HashMap<String,ERReference>();
        //ArrayList<ERReference> resul = new ArrayList<ERReference>();
        for (ERRelationShip rel:this._relaciones)
        {
            //System.out.println("Buscando en relacion " + rel.getName());
            for (EREntity ent: rel.getMembers())
            {
                //System.out.println("revisando entidad miembro " + ent.getName());
                if (StringUtils.equals(ent.getName(), e.getName()))
                {
                    //System.out.println("Encontrado, " + ent.getName() + " = " + e.getName());
                    for (ERReference ref: rel.getReferences()) {
                        //System.out.println("Comprobando " + ref.toString() );
                        //System.out.println(ref.targetAttrib.getName() + "==" + a.getName() + " YYY " + ref.targetEntity.getName() + "==" + e.getName());
                        if ( (StringUtils.equals(ref.targetAttrib.getName(),a.getName())) &&
                             (StringUtils.equals(ref.targetEntity.getName(), e.getName()))
                           )
                        {
                            //System.out.println(ref.targetAttrib.getName() + " = " +  a.getName());
                            //System.out.println(" referencia comprobada y añadida =  " + ref.toString());
                            String key = ref.sourceEntity.getName() + "." + ref.sourceAttrib.getName();
                            resul.put(key,ref);
                        }

                    }

                }
            }
        }

        return resul.values();
    }

    public void update(Database db) {
        for (EREntity ent:this.getEntityList())
        {
            if (ent.getTable()==null) {
                Table tab = db.findTable(ent.getTableName());
                ent.setTable(tab);
            }
        }
    }






}
